﻿using ChunQiu.Arch.Session.Core.ConfigSection;
using ChunQiu.Arch.Session.Core.Models;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.SessionState;
using MongoDB.Driver.Linq;
using System.IO;

namespace ChunQiu.Arch.Session.Core.SessionProviderStrategy
{
    class MongoDBSessionProvider : SessionStateStoreProviderBase
    {

        #region Ctors

        static MongoDBSessionProvider()
        {
            collection = Helper.GetMongoDBCollection<MongoDBSessionDo>();
            System.Configuration.Configuration cfg = WebConfigurationManager.OpenWebConfiguration(HostingEnvironment.ApplicationVirtualPath);
            sessionStateSection = (SessionStateSection)cfg.GetSection("system.web/sessionState");
            httpCookiesSection = (HttpCookiesSection)cfg.GetSection("system.web/httpCookies");
            var keys = IndexKeys.Ascending("Expires");
            var options = IndexOptions.SetTimeToLive(sessionStateSection.Timeout);
            collection.EnsureIndex(keys, options);
        }

        #endregion

        #region Fields

        static SessionStateSection sessionStateSection;
        static HttpCookiesSection httpCookiesSection;
        static MongoCollection<MongoDBSessionDo> collection;

        #endregion

        #region SessionStateStoreProviderBase Members

        public override SessionStateStoreData CreateNewStoreData(System.Web.HttpContext context, int timeout)
        {
            return new SessionStateStoreData(new SessionStateItemCollection(), SessionStateUtility.GetSessionStaticObjects(context), timeout);
        }

        public override void CreateUninitializedItem(System.Web.HttpContext context, string id, int timeout)
        {
            var session = new MongoDBSessionDo();
            session.SessionId = id;
            session.Created = DateTime.Now;
            session.Expires = DateTime.Now.AddMinutes(sessionStateSection.Timeout.TotalMinutes);
            session.LockDate = DateTime.Now;
            session.LockId = 0;
            session.Timeout = timeout;
            session.Locked = false;
            session.Flags = (int)SessionStateActions.InitializeItem;
            collection.Save(session);
        }

        public override void Dispose()
        {
        }

        public override void EndRequest(System.Web.HttpContext context)
        {
            if (SessionProviderSettings.GetSettings().IsSynchronousSessionIdTimeout)
            {
                if (context.Response.Cookies.Count > 0 && context.Response.Cookies[sessionStateSection.CookieName] != null)
                {
                    context.Response.Cookies[sessionStateSection.CookieName].Expires = DateTime.Now.AddMinutes(sessionStateSection.Timeout.TotalMinutes);
                }
                else if (context.Request.Cookies.Count > 0 && context.Request.Cookies[sessionStateSection.CookieName] != null)
                {
                    var requestCookie = context.Request.Cookies[sessionStateSection.CookieName];
                    var responseCookie = new HttpCookie(requestCookie.Name)
                    {
                        Domain = requestCookie.Domain,
                        Expires = DateTime.Now.AddMinutes(sessionStateSection.Timeout.TotalMinutes),
                        HttpOnly = requestCookie.HttpOnly,
                        Path = requestCookie.Path,
                        Secure = requestCookie.Secure,
                        Value = requestCookie.Value,
                    };
                    context.Response.Cookies.Add(responseCookie);
                }
            }
        }

        public override SessionStateStoreData GetItem(System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetSessionStoreItemNolock(false, context, id, out locked, out lockAge, out lockId, out actions);
        }

        public override SessionStateStoreData GetItemExclusive(System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetSessionStoreItemNolock(true, context, id, out locked, out lockAge, out lockId, out actions);
        }

        public override void InitializeRequest(System.Web.HttpContext context)
        {
        }

        public override void ReleaseItemExclusive(System.Web.HttpContext context, string id, object lockId)
        {
            var query = Query.And(Query.EQ("_id", id), Query.EQ("LockId", int.Parse(lockId.ToString())));
            var session = collection.FindOne(query);
            if ((session.Expires - DateTime.Now).TotalMinutes < (session.Timeout / 2))
            {
                var update = Update.Set("Locked", false).Set("Expires", DateTime.Now.AddMinutes(sessionStateSection.Timeout.TotalMinutes));
                collection.Update(query, update);
            }
        }

        public override void RemoveItem(System.Web.HttpContext context, string id, object lockId, SessionStateStoreData item)
        {
            var query = Query.And(Query.EQ("LockId", int.Parse(lockId.ToString())), Query.EQ("_id", id));
            collection.Remove(query);
        }

        public override void ResetItemTimeout(System.Web.HttpContext context, string id)
        {
            var query = Query.And(Query.EQ("_id", id));
            var session = collection.FindOne(query);
            if ((session.Expires - DateTime.Now).TotalMinutes < (session.Timeout / 2))
            {
                var update = Update.Set("Expires", DateTime.Now.AddMinutes(sessionStateSection.Timeout.TotalMinutes));
                collection.Update(query, update);
            }
        }

        public override void SetAndReleaseItemExclusive(System.Web.HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem)
        {
            MongoDBSessionDo session;
            if (newItem)
            {
                var query = Query.And(Query.EQ("_id", id), Query.LT("Expires", DateTime.Now));
                collection.Remove(query);
                session = new MongoDBSessionDo();
                session.SessionId = id;
                session.Created = DateTime.Now;
                session.Expires = DateTime.Now.AddMinutes(sessionStateSection.Timeout.TotalMinutes);
                session.LockDate = DateTime.Now;
                session.LockId = 0;
                session.Timeout = item.Timeout;
                session.Locked = false;
                session.Flags = (int)SessionStateActions.None;
                session.SessionItems = Helper.Serialize((SessionStateItemCollection)item.Items);
                collection.Save(session);
            }
            else
            {
                var query = Query.And(Query.EQ("_id", id), Query.EQ("LockId", int.Parse(lockId.ToString())));
                session = collection.FindOne(query);
                var update = Update
                    .Set("Expires", DateTime.Now.AddMinutes(item.Timeout))
                    .Set("SessionItems", Helper.Serialize((SessionStateItemCollection)item.Items))
                    .Set("Locked", false);
                collection.Update(query, update);
            }
        }

        public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
        {
            return false;
        }

        #endregion

        #region Private Methods

        SessionStateStoreData GetSessionStoreItem(bool lockRecord, System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            SessionStateStoreData item = default(SessionStateStoreData);
            lockAge = TimeSpan.Zero;
            lockId = null;
            locked = false;
            actions = 0;

            bool foundRecord = false;
            bool deleteData = false;

            var session = collection.AsQueryable().FirstOrDefault(s => s.SessionId == id);

            if (lockRecord)
            {
                if (session == null || (session.Locked && session.Expires <= DateTime.UtcNow))
                {
                    locked = true;
                }
                else
                {
                    collection.Update(Query.EQ("_id", id), Update.Set("Locked", true).Set("LockDate", DateTime.Now));
                    locked = false;
                }
            }

            if (session != null)
            {
                if (session.Expires < DateTime.Now)
                {
                    locked = false;
                    deleteData = true;
                }
                else
                {
                    foundRecord = true;
                }
            }

            if (deleteData)
            {
                var deletaQuery2 = Query.EQ("_id", id);
                collection.Remove(deletaQuery2);
            }

            if (!foundRecord)
                locked = false;

            if (foundRecord && !locked)
            {
                lockId = lockId == null ? 0 : (int)lockId + 1;
                collection.Update(Query.EQ("_id", id), Update.Set("LockId", (int)lockId).Set("Flags", (int)SessionStateActions.None));
                if (actions == SessionStateActions.InitializeItem)
                {
                    item = CreateNewStoreData(context, (int)sessionStateSection.Timeout.TotalMinutes);
                }
                else
                {
                    item = Helper.Deserialize(context, session.SessionItems, session.Timeout);
                }
            }

            return item;
        }

        SessionStateStoreData GetSessionStoreItemNolock(bool lockRecord, System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            SessionStateStoreData item = default(SessionStateStoreData);
            lockAge = TimeSpan.Zero;
            lockId = null;
            locked = false;
            actions = 0;

            var session = collection.AsQueryable().FirstOrDefault(s => s.SessionId == id);
            if (session != null && session.Expires >= DateTime.Now)
            {
                lockId = lockId == null ? 0 : (int)lockId + 1;
                if (session.Flags > 0)
                {
                    collection.Update(Query.EQ("_id", id), Update.Set("LockId", (int)lockId).Set("Flags", (int)SessionStateActions.None));
                }
                if (actions == SessionStateActions.InitializeItem)
                {
                    item = CreateNewStoreData(context, (int)sessionStateSection.Timeout.TotalMinutes);
                }
                else
                {
                    item = Helper.Deserialize(context, session.SessionItems, session.Timeout);
                }
            }

            return item;
        }

        #endregion

    }
}
